<?php
/**
 * //无参get
 * $res = Requests::get('http://www.baidu.com/s')->text();
 * //带参get
 * $res = Requests::get('http://www.baidu.com/s',['wd'=>'测试'])->text();
 *
 * //带参post
 * $res = Requests::post('http://httpbin.org/post',['wd'=>'测试'])->json();
 * print_r($res);
 *
 * //带参post，设置参数
 * $res = Requests::post('http://httpbin.org/post',['wd'=>'测试'],['referer'=>'http://www.baidu.com'])->json();
 * print_r($res);
 *
 * //带参post，设置参数
 * $res = Requests::post('http://httpbin.org/post',['wd'=>'测试'],['json'=>true])->json();
 *
 * //ajax请求，解析jsonp
 * $res = Requests::ajax('https://tuijian.suning.com/recommend-portal/dyBase.jsonp?cityId=537&sceneIds=25-73&count=20&districtCode=5370199','',['dataType'=>'jsonp'])->jsonp();
 * print_r($res);
 *
 * //ajax发送post请求
 * $res = Requests::ajax('http://httpbin.org/post','value=1234',['type'=>'post'])->json();
 * print_r($res);
 *
 * //实现 cookie 登录保持会话
 * $session = Requests::session();
 * $res = $session->get('http://www.baidu.com/s','value=1234')->text();
 * $res = $session->post('http://httpbin.org/post','value=1234')->text();
 */

/**
 * @method static Requests get($url, $data = '', $params = [])
 * @method static Requests post($url, $data = '', $params = [])
 * @method static string text()
 * @method static string json()
 *
 * Class Requests
 */
class Requests
{
    private $ch = null;
    private $sh = null;
    private $raw = '';
    private $body = '';
    private $headers = '';

    //默认配置
    private $config = [
        'timeout' => 15,   //超时时间
        'connTimeout' => 15,   //链接超时
        'referer' => null, //来源地址
        'allowRedirects' => true, //跟踪跳转
        'json' => false, //设置Content-Type=application/json
        'cookie' => null,
        'cookieFile' => null,
        'cookieJar' => null,
        'encoding' => true,
        'asyn' => false,
        'userAgent' => ['Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36'],
        'headers' => [
            'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
            'Accept-language' => 'zh-CN,zh;q=0.8',
            'Accept-Charset' => 'utf-8, GB2312;q=0.7,*;q=0.7',
            'Cache-Control' => 'no-cache',
        ]
    ];

    public function __construct()
    {
        $this->sh = curl_share_init();
        curl_share_setopt($this->sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE);
    }

    /**
     * @param string $method
     * @param $url
     * @param array $data
     * @param array $params
     * @return $this|void
     */
    public function request($method, $url, $data = [], $params = [])
    {

        if (empty($url)) return;
        $this->config = array_merge($this->config, $params);

        switch (strtoupper($method)) {
            case 'POST':
                $options[CURLOPT_HTTPGET] = false;
                $options[CURLOPT_POST] = true;
                if ($this->config['json'] == true) {
                    $this->config['headers']['Content-Type'] = 'application/json';
                    $options[CURLOPT_POSTFIELDS] = json_encode($data, JSON_UNESCAPED_UNICODE);

                } else {
                    $options[CURLOPT_POSTFIELDS] = is_array($data) ? http_build_query($data) : $data;
                }
                break;
            case 'GET':
                if (!empty($data)) $url .= (strpos($url, '?') ? '&' : '?') . http_build_query($data);
                $options[CURLOPT_HTTPGET] = true;
                $options[CURLOPT_POST] = false;
                break;
            case 'AJAX':

                $type = strtoupper($this->config['type'] ?? 'GET');
                $dataType = strtoupper($this->config['dataType'] ?? 'TEXT');

                if ($dataType === 'JSON') {
                    $this->config['headers']['Accept'] = 'application/json';
                } elseif ($dataType === 'JSONP') {
                    $this->config['headers']['Accept'] = '*/*';
                } else {
                    $this->config['headers']['Accept'] = 'text/javascript, application/javascript, application/ecmascript, application/x-ecmascript, */*; q=0.01';
                }

                if ($type === 'POST' && $dataType !== 'JSONP') {
                    $options[CURLOPT_POST] = true;
                    $options[CURLOPT_POSTFIELDS] = is_array($data) ? http_build_query($data) : $data;
                    $this->config['headers']['Content-Type'] = 'application/x-www-form-urlencoded';
                } else {
                    if (!empty($data)) $url .= (strpos($url, '?') ? '&' : '?') . http_build_query($data);
                    if ($dataType === 'JSONP') {

                        if (!$this->config['callback']) $this->config['callback']['callback'] = 'jsonp_' . $this->msectime();
                        $this->config['callback']['_'] = $this->msectime();

                        $url = $this->query_url($url, $this->config['callback']);
                    } else {
                        $this->config['headers']['Host'] = parse_url($url)['host'];
                    }
                }

                $this->config['headers']['X-Requested-With'] = 'XMLHttpRequest';

                break;
        }

        //判断是不是https
        if (substr($url, 0, 8) == 'https://') {
            $options[CURLOPT_SSL_VERIFYHOST] = false;
            $options[CURLOPT_SSL_VERIFYPEER] = false;
        }

        $headers = [];
        foreach ($this->config['headers'] as $key => $value) {
            $headers[] = $key . ':' . $value;
        }

        $options[CURLOPT_HEADERFUNCTION] = function ($ch, $header) {
            if ($this->_status_code() == 200) $this->headers .= $header;
            return strlen($header);
        };

        $options[CURLOPT_SHARE] = $this->sh;
        $options[CURLINFO_HEADER_OUT] = false;
        $options[CURLOPT_HEADER] = false;
        $options[CURLOPT_RETURNTRANSFER] = true;
        $options[CURLOPT_AUTOREFERER] = true;
        $options[CURLOPT_FOLLOWLOCATION] = $this->config['allowRedirects'];
        $options[CURLOPT_TIMEOUT] = $this->config['timeout'];
        $options[CURLOPT_CONNECTTIMEOUT] = $this->config['connTimeout'];
        $options[CURLOPT_ENCODING] = $this->config['encoding'];
        $options[CURLOPT_HTTPHEADER] = $headers;  //设置header
        $options[CURLOPT_USERAGENT] = is_array($this->config['userAgent']) == true ? $this->config['userAgent'][array_rand($this->config['userAgent'], 1)] : $this->config['userAgent'];  //设置userAgent

        if (!empty($this->config['cookie'])) $options[CURLOPT_COOKIE] = $this->config['cookie']; //设置cookie
        if (!empty($this->config['referer'])) $options[CURLOPT_REFERER] = $this->config['referer']; //设置referer
        if (!empty($this->config['cookieFile'])) $options[CURLOPT_COOKIEFILE] = $this->config['cookieFile']; //设置cookieFile
        if (!empty($this->config['cookieJar'])) $options[CURLOPT_COOKIEJAR] = $this->config['cookieJar']; //保存cookieJar

        $this->exec($url, $options);
        return $this;
    }

    private function msectime()
    {
        list($msec, $sec) = explode(' ', microtime());
        return (float)sprintf('%.0f', (floatval($msec) + floatval($sec)) * 1000);
    }

    private function query_url($url, $args = array())
    {

        $url = parse_url($url);

        $params = array();
        $newUrl = '';
        parse_str($url['query'] ? $url['query'] : $url['path'], $params); //To array

        if (!empty($args)) {
            foreach ($args as $key => $value) {
                $params[$key] = $value;
            }

            if ($url['scheme']) $newUrl .= $url['scheme'] . '://';
            if ($url['host']) $newUrl .= $url['host'];
            if ($url['path']) $newUrl .= $url['path'];
            $newUrl .= '?' . http_build_query($params);

            return $newUrl;
        }
    }

    /**
     * @param $data
     * @return array|null
     */
    private function analy_json($data)
    {
        $data = htmlspecialchars_decode($data);
        $result = json_decode($data, true, JSON_UNESCAPED_UNICODE);

        if (empty($result)) {

            $data = str_replace('\\', '', $data);

            //key无双引号时自动不全；
            if (preg_match('/\w:/', $data)) $data = preg_replace('/(\w+):/is', '"$1":', $data);
            if (preg_match('/\'\w:/', $data)) $data = preg_replace('/\'(\w+)\':/is', '"$1":', $data);

            //值无双引号时自动不全；
            if (preg_match('/:\'.*/', $data)) $data = preg_replace('/:\'(.*)\'/is', ':"$1"', $data);

            $result = json_decode($data, true, JSON_UNESCAPED_UNICODE);

        }
        return $result;
    }

    private function jsonp_decode($data)
    {
        if (preg_match('/' . $this->config['callback']['callback'] . '\((.*?)}\)/is', $data, $match)) {
            return isset($match[1]) ? $this->analy_json(trim($match[1]) . '}') : '';
        } else {
            return false;
        }
    }

    //检查字符集
    private function detect_encoding($string)
    {
        //$string = '进行成功的跳转';
        $encode = ['UTF-8', 'GB2312', 'BIG5', 'UTF-16LE', 'UTF-16BE', 'ISO-8859-1'];
        foreach ($encode as $value) {
            $tmp = $this->convert_charset($string, $value);
            if (md5($tmp) == md5($string)) return $tmp;
        }
        return false;
    }

    private function convert_charset($string, $from = 'GB2312', $to = 'UTF-8')
    {

        if (strtoupper($from) === strtoupper($to) || empty($string) || (is_scalar($string) && !is_string($string))) {
            return $string;
        }

        if (function_exists('mb_convert_encoding')) {
            return mb_convert_encoding($string, $to, $from);
        } elseif (function_exists('iconv')) {
            return iconv($from, $to, $string);
        } else {
            return $string;
        }
    }

    //执行请求
    private function exec($url = '', $options)
    {
        try {
            $this->ch = curl_init($url);
            curl_setopt_array($this->ch, $options);
            $this->body = curl_exec($this->ch);
            if (!curl_error($this->ch)) {
                $this->body = $this->detect_encoding($this->body);
            }
        } catch (Exception $e) {
        }
    }

    public function _raw()
    {
        return $this->headers . $this->body;
    }

    public function _header()
    {
        return $this->headers;
    }

    public function _text()
    {
        return $this->body;
    }

    public function _json()
    {
        return $this->analy_json($this->body);
    }

    public function _jsonp()
    {
        return $this->jsonp_decode($this->body);
    }

    private function _get($url, $data = '', $params = [])
    {
        return $this->request('GET', $url, $data, $params);
    }

    private function _post($url, $payload = '', $params = [])
    {
        return $this->request('POST', $url, $payload, $params);
    }

    private function _ajax($url, $data = '', $params = [])
    {
        return $this->request('AJAX', $url, $data, $params);
    }

    //返回cookie
    public function _cookie($text = true)
    {
        $cookie = curl_getinfo($this->ch, CURLINFO_COOKIELIST);
        if ($text === true) {
            $cookieStr = '';
            foreach ($cookie as $value) {
                $value = preg_replace('/\s+/', ',', $value);
                $value = array_reverse(explode(',', $value));
                $cookieStr .= $value[1] . '=' . $value[0] . ';';
            }
            $cookie = substr($cookieStr, 0, strlen($cookieStr) - 1);
        }
        return $cookie;
    }

    //响应状态码
    public function _status_code()
    {
        return curl_getinfo($this->ch, CURLINFO_RESPONSE_CODE);
    }

    //获取访问的url
    public function _url()
    {
        return curl_getinfo($this->ch, CURLINFO_EFFECTIVE_URL);
    }

    //获取网页title
    public function _title()
    {
        if ($this->body) preg_match('/<title>(.*?)<\/title>/is', $this->body, $match);
        return $match ? $match[1] : '';
    }

    public function _session($options = [])
    {
        $this->config = array_merge($this->config, (array)$options);
        return $this;
    }

    public function __call($method, $args = [])
    {
        return call_user_func_array([$this, '_' . $method], $args);
    }

    public static function __callStatic($method, $args)
    {
        return call_user_func_array([new self, '_' . $method], $args);
    }

    public function __destruct()
    {
        curl_share_close($this->sh);
        if ($this->ch) curl_close($this->ch);
        $this->config = null;
        $this->headers = null;
        $this->raw = null;
        $this->body = null;
        $this->ch = null;
        $this->sh = null;
    }
}